테스트의 망령에 사로잡히다
나는 수능이 끝나고 대학가기 전의 자유로운 시기에 본격적으로 프로그래밍을 시작했다. 그렇게 만들고 싶은 것을 만들면서 실력을 늘려온지가 벌써 8년 쯤 된다. 그런데 TDD 등 개발을 위한 테스팅 기법을 연구한 것은 7년 쯤 된다. 와우!
오늘은 내가 어쩌다 테스팅 기법에 그렇게 집착하게 되었는지, 그러다 어떤 식으로 대가리가 깨졌는지, 어떻게 봉합되었는지에 대해 알아보도록 하자.
TDD에 매료되다
내가 TDD를 알아보기 시작한건 무려 학부 1학년 말이었다. 그 해 겨울, 켄트벡의 그 책을 거실 탁자에서 보던 기억이 새록새록 난다. 당시 학식 1학년이었던 나는 일(학교)로는 C를, 취미로는 죽은 아들 부랄 플래시 액션 스크립트3(AS3)를 쓰면서 게임을 만들고 있었다. 1학기 때는 AS3로 슬라이딩 숫자 퍼즐[1]을 만들기도 했고, 여름 방학 때는 한 달 반을 들여 C 콘솔 고누 게임을 만들기도 했다.
이것들을 만들면서 든 생각은 아마 이랬을 것이다
- 내가 짠 코드가 맞는지 어떻게 알 수 있을까?
- 짜고 나서 버그를 잡는 것이 너무 버겁다
TDD의 존재를 알게 된 뒤, 나는 켄트 벡의 책을 읽으며 과제를 하거나 개인 프로젝트를 할 때 TDD를 시도해보기 시작한다. 나는 곧 TDD의 효과에 매료되었다. 실패하는 작은 테스트를 작성하고 그것을 통과하는 방식의 코딩은, 기존에 왕창 짜고 왕창 디버깅하는 방식보다 훨씬 효과적이었다.
숫자 퍼즐을 만들던 시절에는 심지어 컴파일도 없이 몇시간을 짜다가 다 만들었다 싶었을 때 실행을 해보고 수도 없이 나오는 버그를 고치면서 고생을 했었다. TDD를 알고 적용하면서부터는 개발이 마치 양파 껍질 까듯이 진행되었다. 다 짜고 버그를 잡는 것이 아니라, 버그를 미리 잡으면서 점진적으로 진행하는 것이다. 이런 방식은 아주 효과적이면서 심리적인 안정감마저 제공했었다.
그렇게 TDD에 입문을 하고 2학년 때부터는 거의 모든 일에 TDD를 적용하고 다녔다. 개인 프로젝트를 할 때는 물론이고 학교 과제에도 TDD를 적용했다. 다 괜찮은 듯 싶었다.
TDD.. 정말 좋은 거 맞나?
알고리즘 주간 과제
그런데 3학년 2학기가 되고 알고리즘 수업에서부터 TDD에 대한 회의감을 느끼기 시작한다. 이 수업은 일주일마다 백준 비슷하게 알고리즘 문제가 주어지고, 매주 문제를 해결해야만 했다. 그리고 우수한 코드는 데드라인 이후 답지와 함께 공개되는 형식이었다. 나는 이 수업 과제에서도 역시 TDD를 적용했다. 항상 점진적인 개발을 하며 좋은 코드를 짜고 있다고 생각했다.
그런데 웬걸, 공개되는 우수 코드들을 보면 그렇지 않았다. TDD를 하면서 짰다는 나보다 훨씬 간결한 코드가 공개되어 있었고, 나는 뭔가 잘못되었다고 생각했다. 하지만 TDD 방식을 멈출 수는 없었다. TDD에 너무 심취해서, 양파 까듯 버그를 잡으면서 짜지 않으면 문제가 생길까봐 불안해질 정도였다. 그리고 너무 바빠서 무엇이 잘못된 것인지 분석해 볼 시간도 여유도 없었다. 그냥 뭔가 TDD가 잘못된 것이 아닐까? 어렴풋이 느낄 뿐이었다.
그 당시에 과거 블로그에 썼던 글이다
- 알고리즘(최적화) 과제를 하면서 얻은 것들: TDD의 유용성을 의심하다가 갈! 의심하지 말지어다! 믿는 자에게 복이 오리니.. 오오 테스트시여.. 이러고 있네 ㅋㅋㅋㅋ
- 알고리즘 문제를 풀면 겸손해진다: 이 당시 굉장히 스트레스도 많이 받고 나는 이것 밖에 안 되나 하면서 자존심이나 자신감에 스크래치가 가기도 했었다.근데 식질머신 이야기가 있네? 무려 17년도부터 구상 중이었네..
그냥 제대로된 알고리즘을 생각해내야 되는데, TDD를 한다면서 생각없이 설친 것이 시간 낭비가 되었던 것 같다.
컴파일러 텀 프로젝트
이 시기에 또 내가 패배 선언한 과제가 있는데, 원하는 언어의 컴파일러/인터프리터를 하나 만드는 게 컴파일러 텀 프로젝트였다. 다른 학생들은 보통 C 컴파일러를 만들었지만, 다들 자바로 싱글 페이지 앱 만들 때 혼자 2D 액션 게임을 만드는 기행을 저질렀던 나는 과제로 또 이상한 걸 만들었다. 컴파일러 텀프로 LISP의 방언인 scheme 인터프리터를 만들고자 한 것이다. 물론 풀피처는 아니고 아주 일부만 구현한 미니멀 스킴 - minscheme이었다. 이걸 lex, yacc, C++로 만들어야 했다.
근데 개같이 멸망했다. ㅈ빠지게 고생했지만 툭하면 세그폴트 터지고, 람다 함수는 겨우겨우 누더기 골렘처럼 어찌어찌 구현하고, 재귀는 한참 전에 물건너 간 상태였다. 더 쪽팔리는 건 컴파일러는 소수 정예 수업이라 면접 형식으로 교수님 앞에서 이걸 보여주면서 설명을 해야 했던 것이다.
- 어떻게 만든거니?
- 몰라요.. 저두 모르겠음.. 몬가.. 몬가 일어나구 있음..
교수님의 떨떠름한 표정과 음성이 아직도 잊혀지지 않는다.
SICP를 완독했더라면 충분히 만들 수 있었을테지만 2장까지만 읽었었고.. 이 때 나는 애초에 리슾 인터프리터를 만들 수 있는 지식 자체가 부족했다. 설상가상 컴파일러 수업에서는 컴파일러의 프론트엔드만 강조하면서 Lex Yacc과 무슨 킹론상은 좋지만 실제로는 아무도 안 쓴다는 LR 파서 이론 이딴 것만 가르쳐 주고, 인터프리터 개발에 필수적인 "실행 환경"은 아예 언급도 없었다. 사실 좀 부조리한 과제이긴 하다. 한 학기 내내 컴파일러 앞단만 알려주고 무슨 컴파일러 전부를 짜라는 거야.. 어떻게 하는 건데..
이 짓을 하고 있을 때 작성한 것들이다
- https://github.com/KUR-creative/minscheme/blob/master/interpreter-test.cpp 수도 없이 많은 테스트 코드를 작성했다. 근데 아무 쓸모도 없었던 기억이 난다. 결국 TDD는 로직의 핵심을 모르면 아무 쓸모가 없다.
- https://m.blog.naver.com/rhdnfka94/221170445495 민스킴 관련으로 과거 블로그에 쓴 글이다. 역시 나만 보는 글로 적어 놨네..
결론적으로 잘 알지도 못하는데 TDD 하면서 테스트 코드 짜는 똥꼬쇼 하고 있을 시간에 리슾 인터프리터에 대해 더 제대로 공부했더라면 제대로 된 걸 짤 수 있었을지도 모른다.
이 때의 결론
그런 TDD로 괜찮은가?
괜찮다. 문제없어
(23년의 나: 문제 많어 미1친놈아!)
사실 알고리즘이나 컴파일러 텀프를 망친 이유는, TDD 그 자체의 문제라기보다 결국 도메인이나 작성해야 하는 로직이 명확하지 않고 문제를 제대로 이해하지 못해서이다. TDD는 그냥 거기에 시간 낭비를 살포시 얹어줬을 뿐이다.
물론 이건 TDD를 하는 사람들이 흔히 겪는 주화입마이긴 하다. 뭘 제대로 알지도 못하는 상황에서 아무튼 하면 되겠지 하고 TDD를 썼다가 시간만 질질 끌리고 결국 피를 보고야 마는 것이다. 특히 TDD가 설계의 도구라느니 로직이 명확해진다느니 하면서 낚아대는 사람들 잘못도 있고..
이 때는 지금처럼 명확하게 문제의 원인을 짚지는 못했지만, 아마 뭔가 잘못되고 있다는 느낌만큼은 받았던 것 같다. 하지만 TDD를 내려 놓지는 못했다. 블로그 글마다 TDD 쓸모 없는 것 같은데... 라고 생각하다가도 갈!!! 니가 잘못 쓴거야!!!!! 하는 걸 보면..
암튼 아직 대가리 봉합 안 된 시기였다.
PBT에 매료되다
그리고 4학년이 되어 졸작으로 식질머신을 만들고, 석사 1학년 때는 새로 학습시킨 모델에 PyQt GUI를 붙여서 여러분이 아는 식질머신v0를 만들고 배포한다. 딥러닝 학습 코드나 구이는 아무래도 TDD를 할 만한 부분이 별로 없었다. 그래서 한 동안 잊고 살았는데..
고급자료구조론
석사 2학년 1학기에 고급자료구조론을 들으면서 Property Based Testing, PBT를 시험해보기 시작한다. 이 시기에 만든 과제들을 여기서 볼 수 있다: https://github.com/KUR-creative?tab=repositories&q=ads 나도 방금 이 때 했던 과제들을 보고 왔는데, 재미 있는 게 많네. 흥미로운 자료구조들을 구현하고 실험 결과를 깔끔하게 제시한 레포트들이 많다. 크~~~ 역시 나야 개잘해
깃헙 링크를 열어 보면 최근에 만든 레포부터 정렬되어 있다. 그런데 3번째 과제(binomial heap)까지는 C로 구현하고 C++의 catch 테스트 라이브러리로 테스트하다가 4번째 과제(ads-mini-dyn-array)부터는 갑자기 Python을 쓰면서 hypothesis라는 테스트 라이브러리를 쓰기 시작하고, 이후에는 끝까지 그렇게 한다. 심지어 마지막 과제인 ads-final에서는 C로 짠 자료구조 코드를 DLL로 만든 다음 파이썬에서 ctypes로 불러와서 하이포띠시스로 테스팅을 한다.
나도 몰랐었는데 딱 저 시기부터 PBT를 적용해보면서 빠져들기 시작한 거 같다.
Property Based Testing
PBT는 프로그램이 반드시 만족해야 하는 속성property을 테스트하는 방법론이다.
예를 들어 정수 배열을 오름차순 정렬하는 함수 sort를 짠다고 해보자. sort의 속성은
- 입력과 출력 배열의 길이는 동일해야 하고
- 출력 배열의 원소는 1 이상의 모든 인덱스
i
에 대해a[i-1] <= a[i]
를 만족해야 한다.
그러면 이런 속성을 어떻게 테스트할까? 무작위 입력을 자동으로 생성하여 테스트할 수 있다
- 함수의 입력을 무작위로 생성하는 로직을 PBT 라이브러리로 짠다
- 1에서 생성한 입력을 함수에 넣어 출력을 만들고
- 입출력이 어떤 속성(Property)을 만족하는지 테스트한다
sort의 입력은 무작위 정수가 있는 무작위 크기의 배열이다.
Hypothesis라는 파이썬 PBT 라이브러리로 표현하면 다음과 같다.
from hypothesis import given
from hypothesis import strategies as st
gen_lst = st.lists(st.integers())
속성은 다음과 같이 표현된다.
@given(gen_lst)
def test_my_sort(in_lst):
out_lst = sort(in_lst)
# 1. 길이는 같아야 한다.
assert len(out_lst) == len(in_lst)
# 2. E_n <= E_n+1
for a, b in zip(out_lst[:-1], out_lst[1:]):
assert a <= b
위 예시에 대한 영상이다:
PBT 예시 라이브 코딩|embed
만일 sort의 구현이 잘못되어 속성을 만족시키지 못했다고 하자.
이 때 PBT 라이브러리의 마법, Shrink가 일어난다.
- 무작위로 생성한 거대한 입력, 예를 들어 20~40자리 정수가 100개 있는 리스트로 테스트해 보니 함수의 출력이 속성을 만족시키지 못했다고 하자
- 그러면 PBT 라이브러리는 입력의 크기를 줄여서 다시 테스트해 본다. 10~20자리 정수 50개 있는 리스트로 크기를 줄여서 다시 테스트한다. 여전히 속성을 만족시키지 못한다면 이를 반복한다.
- 테스트를 통과하는 최소 크기의 예시가 나오면, 바로 이전(즉 테스트가 실패하는) 단계의 입력과 출력을 반례 데이터로 제시한다.
그러면 사용자는 제시된 반례 데이터로 함수를 실행해보고, 문제를 확인한 뒤 구현을 수정할 수 있다.
다음은 매우 복잡한 자료구조인 B-tree(정확히는 2-3-4 트리)를 PBT로 테스팅하는 예시 영상이다.
B-tree 테스트 영상|embed
55:01에서 코드를 수정했을 때 테스트가 실패하고, 매우 작은 반례 데이터를 제시하는 것을 확인할 수 있다.
PBT 개쩔어! 지렸다...
PBT를 간략하게 설명해 보았는데, 그 강력함에 공감할 수 있을까? 영상을 꼭 봤으면 좋겠다.
위 영상을 찍었을 때 나는 TDD를 5년을 써 왔던 시점이었다. 하지만 TDD의 불완전함에 불만이 있었고, 대체할 만한 방법론을 찾고 있었다. 그러다 PBT를 찾았고, 고급자료구조론 과제에서 5번을 사용해보고 5번 모두 대성공을 한다.
PBT를 쓰면 내가 정의한 스펙에서는 버그가 절대로 발생하지 않는다. 제정신인 프로그래머라면 버그에 대해서 이렇게 강한 주장은 절대 하지 않을 것이다. 하지만 5번을 내리 PBT를 경험해 본 나의 결론은 이랬다
- PBT로 테스트한 코드는 버그가 없는 것이 보장되어 있다.
- 버그가 생긴다면 스펙(테스트 코드)이 잘못된 것이다.
경험해보지 않은 사람은 믿기 힘들겠지만 정말이다. 나는 계속해서 의심했고, 특히 마지막 과제는 코딩 중에 버그가 미친듯이 발생했기에 수도 없이 의심했다. PBT는 통과하지만 구현은 틀린 거 아닐까? 코딩하면서 수십번을 넘게 그런 생각을 했다. 그런데 아니었다. PBT로 검증된 코드는 정말로 버그가 없었다.
처음에는 믿기지 않았는데, 이윽고 확신하게 된다.
이건 진짜구나.
내가 정말로 버그를 완전히 없애는 법을 찾아냈구나!!
그러고 나니까
왜 더 많은 사람들이 이걸 쓰지 않지?
다들 뭐하고 있는 거지?
이런 생각마저 들었다.
내가 느낀 감격이 잘 전해졌을지 모르겠다.
세상에 이런 게 있다니! 보물을 찾아낸 심정이었다.
그리고 오래오래 행복하게 살았습니다?
그랬겠냐? 그럼 제목이 저딴 식이지도 않았겠지.
하지만 글이 너무 길어졌으니까 여기서 한번 자르도록 하자.
지금까지의 모든 내용을 깔끔하게 잘 정리해서 떠먹여 주는 영상이 있다.
생각을 그대로 프로그래밍하는 방법
part 1: 학부생의 방법론 / TDD 방법론
part1|embed
part 2: PBT, REPL
part2|embed
위 영상에선 내 잡다한 경험은 전부 제거하고 나의 학부 1학년 코딩 방식, TDD, PBT의 핵심 개념과 실천 방법을 라이브 코딩으로 소개한다. 보너스로 REPL 기반 개발도 살짝 소개한다. 그리고 각 방법론의 여러가지 속성을 테이블로 정리하여 비교하고 평가한다. 만드는데 엄청나게 공들였고 저 당시(21년)까지 내가 연구한 모든 프밍 방법론을 집대성한 영상이니까 꼭 봐줬으면 좋겠다.